﻿/*
 * Twipler - Twitter Web Client with features.
 *  
 *  Copyright (C) 2009, 2010. Ian Quigley
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details. 
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Twipler.App.Machine;
using Twipler.Xtensions;

namespace Twipler.App.Parse.Code
{
    public class TokenParser : StringParser
    {
        public const string Alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        public const string Numeric = "0123456789";
        public const string ItemNameText = Alpha + Numeric + "+-_";

        private IEnumerable<Type> _callableClasses;

        public enum Token
        {
            Any,
            Value,          // ie. number, string or code which produces a value
            Declaration,    // ie. keyword or variable
            Operator        // ie. =, ==, +, *
        }

        public enum TokenType
        {
            Any,
            ItemName,
            StaticMethodCallParam,
            StaticMethodCall,
            InstructionList,
            Integer,
            String,
            TimeSpan
        }

        public class Result
        {
            public Token Is;
            public object Value;
        }

        public class ResultEmpty : Result
        { }

        public class ResultError : Result
        {
            public string Message;
        }

        public TokenParser(string content) : base(content)
        {
            PopulateCallableClasses();
        }

        public TokenParser(StringParser switchTo) : base(switchTo)
        {
            PopulateCallableClasses();
        }

        private void PopulateCallableClasses()
        {
            _callableClasses = from type in Assembly.GetExecutingAssembly().GetTypes()
                               where typeof(ICallableClass).IsAssignableFrom(type)
                               select type;
        }

        public TokenParser(string content, IEnumerable<Type> classes): base(content)
        {
            _callableClasses = classes;
        }

        public Result Next(TokenType tokenType)
        {

            // Nice yummy factory-strategy pattern...

            ConcreteExpectation expectation = ConcreteExpectation.Get(tokenType);
            if (expectation != null)
                return expectation.Parse(this);


            // Here follows some butt-ugly code

            SkipWhiteSpace();

            if (IsEmpty)
                return new ResultEmpty() { Is = Token.Any };

            if (LooksLikeString() && IsExpectingValue(tokenType))
                return ReadStringOrError();

            // Read instruction list
            if (LooksLikeString() && (tokenType == TokenType.InstructionList) && SkipChar('"'))
            {
                CodeParse codeParse = CodeParse.SwitchFrom(this);

                List<Instruction> instructionList = codeParse.ReadInstructions();

                Digest(codeParse);

                return !SkipChar('"') ? new ResultError() { Message = "Missing close \"" } : new Result() {Value = instructionList};
            }


            // Can be something wrapped in "" but still ignore whitespace
            bool looksLikeString = LooksLikeString();
            if (looksLikeString)
                SkipChar('"');

            string allowChars = ItemNameText;
            if (tokenType == TokenType.StaticMethodCallParam)
                allowChars = "?" + ItemNameText;

            string sniffWord = ReadWhileInWhiteList(allowChars);
			
			// special case for "null"
			if (sniffWord.Equals("null", StringComparison.OrdinalIgnoreCase))
			{
				if (looksLikeString)
					SkipChar('"');

				return new Result() {Is = Token.Value, Value = null};
			}

            if (IsACallableClass(sniffWord) && Peek(".", true))
            {
                Result r = ReadMethodCallOrError(sniffWord);
                if (r is ResultError)
                    return r;

                if ((looksLikeString) && !SkipChar('"'))
                    return new ResultError() {Message = "Missing close \""};

                return r;
            }

            if (sniffWord.ContainsOnly(Numeric + "+-"))
            {
                if (NextCharIs('.'))
                {
                    // Try float
                    SkipChar('.');
                    string remainder = ReadWhileInWhiteList(Numeric);
                    if (remainder.Length == 0)
                        return new ResultError() {Message = "Decimal point not followed by decimal part"};

                    sniffWord += "." + remainder;

                    double d;
                    if (double.TryParse(sniffWord, out  d))
                    {
                        if ((looksLikeString) && !SkipChar('"'))
                            return new ResultError() { Message = "Missing close \"" };

                        return new Result() {Is = Token.Value, Value = d};
                    }
                }
                else
                {
                    // Try Int
                    int i;
                    if (int.TryParse(sniffWord, out i))
                    {
                        if ((looksLikeString) && !SkipChar('"'))
                            return new ResultError() { Message = "Missing close \"" };

                        return new Result() {Is = Token.Value, Value = i};
                    }
                }
            }

            if (IsExpectingValue(tokenType))
            {
                if ((looksLikeString) && !SkipChar('"'))
                    return new ResultError() { Message = "Missing close \"" };

                return new Result() {Is = Token.Value, Value = sniffWord};
            }

            return new ResultError() {Is = Token.Any, Value = "Error"};
        }

        private Result ReadMethodCallOrError(string sniffWord)
        {
            Type t = (from p in _callableClasses
                      where p.FullName.EndsWith(sniffWord)
                    select p).FirstOrDefault();

            if ((t == null) || !SkipChar('.'))
                return new ResultError() { Message = "Error reading " + sniffWord};
            
            string methodName = ReadWhileInWhiteList(Alpha + Numeric);

            IEnumerable<MethodInfo> mi = from method in t.GetMethods(BindingFlags.Static|BindingFlags.Public)
                                         where method.Name == methodName
                                         select method;

            if (mi.Count() == 0)
                return new ResultError() { Message = methodName + " not found in entity " + sniffWord };

            if (!SkipChar('('))
                return new ResultError() { Message = "Expected open bracket" };

            int maxParams, minParams;
            MinMaxParameters(mi, out minParams, out maxParams);

          List<Variable> param = new List<Variable>();

            while (param.Count() <= maxParams)
            {
                if (NextCharIs(')') && SkipChar(')'))
                    break;

                Result nextParam = Next(TokenType.StaticMethodCallParam);
                if (nextParam is ResultError)
                    return nextParam;

                param.Add(new Variable(nextParam.Value));

                if (NextCharIs(','))
                    SkipChar(',');
                else if (NextCharIs(')') && SkipChar(')'))
                    break;
                else
                    return new ResultError() { Message = "Unexpected character '" +  NextChar + "'"};

            }

            IEnumerable<MethodInfo> candidates = from method in mi
                                                 where method.GetParameters().Count() == param.Count()
                                                 select method;

            if (candidates.Count() == 0)
                return new ResultError() 
                { Message = string.Format(
                    "{0} expects between {1} and {2} parameters, but found {3} parameters", 
                    methodName, minParams,maxParams,  param.Count()) };

            if (candidates.Count() > 1)
            {
                candidates = from candidate in candidates
                             where candidate.GetParameters()
                                .All(p => p.ParameterType.IsAssignableFrom(param[p.Position].Type))                             
                             select candidate;
            }

            if (candidates.Count() != 1)
                return new ResultError() { Message = "Ambiguous method signature cannot be resolved by parameter type." };

            return new Result() { Is = Token.Any, Value = new StaticMethodCall(candidates.First(), param) };
            
        }

        private void MinMaxParameters(IEnumerable<MethodInfo> mi, out int min, out int max)
        {
            min = mi.First().GetParameters().Count();
            max = min;

            foreach (MethodInfo method in mi)
            {
                min = Math.Min(method.GetParameters().Count(), min);
                max = Math.Max(method.GetParameters().Count(), max);
            }
        }

        private bool IsACallableClass(string sniffWord)
        {
            return (from p in _callableClasses
                    where p.FullName.EndsWith(sniffWord)
                    select p).FirstOrDefault() != null;
        }

        private Result ReadStringOrError()
        {
            string quotedString = ReadQuotedString();
            if (quotedString == null)
                return new ResultError() { Message = "Error parsing string" };

            return new Result() { Is = Token.Value, Value = quotedString };
        }

        private bool IsExpectingValue(TokenType tokenType)
        {
            return ((tokenType == TokenType.ItemName) || (tokenType == TokenType.StaticMethodCallParam) || (tokenType == TokenType.Any) || (tokenType == TokenType.String));
        }

        private bool LooksLikeString()
        {
            return (Content[Index] == '"');
        }
    }

    public abstract class ConcreteExpectation
    {
        public static ConcreteExpectation Get(TokenParser.TokenType type)
        {
            switch (type)
            {
                case TokenParser.TokenType.TimeSpan:
                    return new TimeSpanExpectaction();

                case TokenParser.TokenType.Integer:
                    return new IntegerExpectaction();

                default:
                    return null;
            }
        }

        public abstract TokenParser.Result Parse(StringParser parser);
        public abstract object ReadType(StringParser parser);

        protected TokenParser.Result WrappedInQuotes(StringParser parser)
        {
            parser.SkipWhiteSpace();

            bool wrappedInString = (parser.NextCharIs('"') && parser.SkipChar('"'));

            object obj = ReadType(parser);

            if (wrappedInString && !parser.SkipChar('"'))
                return new TokenParser.ResultError() { Message = "Missing close quote \". " };

            if (obj != null)
                return new TokenParser.Result() {Is = TokenParser.Token.Any, Value = obj};

            return new TokenParser.ResultError() { Message = "Could not interpret data." }; 
        }
    }

    public class IntegerExpectaction : ConcreteExpectation
    {
        public override TokenParser.Result Parse(StringParser parser)
        {
            return WrappedInQuotes(parser);
        }

        public override object ReadType(StringParser parser)
        {
            if (!parser.NextCharIs(c => c.Is_09()))
                return null;

            int lim = 10;
            int result = 0;
            while (parser.NextCharIs(c => c.Is_09()) && --lim > 0)
            {
                result *= 10;
                result += int.Parse(parser.NextCharRead());
            }

            return result;
        }
    }

    public class TimeSpanExpectaction : ConcreteExpectation
    {
        public override TokenParser.Result Parse(StringParser parser)
        {
            return WrappedInQuotes(parser);
        }

        public override object ReadType(StringParser parser)
        {
            IntegerExpectaction integerReader = new IntegerExpectaction();
            object objInt = integerReader.ReadType(parser);

            if (objInt == null)
            {
                parser.AddError("TimeSpan should start with a number");
                return null;
            }

            parser.SkipWhiteSpace();

            int spanType = "dDhHmMsS".IndexOf(parser.NextChar);
            switch (spanType)
            {
                case 0:
                case 1:
                    parser.SkipChar();
                    return new TimeSpan((int)objInt, 0, 0, 0);

                case 2:
                case 3:
                    parser.SkipChar();
                    return new TimeSpan((int) objInt, 0, 0);

                case 4:
                case 5:
                    parser.SkipChar();
                    return new TimeSpan(0, (int)objInt,  0);

                case 6:
                case 7:
                    parser.SkipChar();
                    return new TimeSpan(0, 0, (int)objInt);

                default:
                    parser.AddError("TimeSpan expects number followed by one of D H M S (day, hour, minute, second)");
                    return null;
            }
        }
    }
}
